package com.strong.utils.security.jwt;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.exceptions.ValidateException;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTPayload;
import cn.hutool.jwt.JWTUtil;
import cn.hutool.jwt.JWTValidator;
import com.strong.config.ProfileConfig;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.Map;

import static cn.hutool.jwt.RegisteredPayload.EXPIRES_AT;
import static cn.hutool.jwt.RegisteredPayload.ISSUED_AT;
import static com.strong.utils.security.SecurityUtils.MAP_SYSTEM_USER_TOKEN;


/**
 * JWT工具类
 *
 * @author simen
 * @date 2022/01/29
 */
@Component
public class JwtTokenUtils {

    public static final String MSG_LOGIN_TOKEN_NOT_EXIST = "用户登录信息不存在";
    public static final String MSG_SIGNATURE_FAILED = "用户签名验证失败";
    public static final String MSG_USER_SIGNATURE_EXPIRED = "用户签名已过期";

    /**
     * JWT用户权限名
     */
    public static final String STR_USER_AUTHORITY = "USER_AUTHORITY";

    /**
     * JWT用户扩展属性
     */
    public static final String STR_MAP_USER_PROPERTIES = "MAP_USER_PROPERTIES";

    /**
     * 到期30分钟
     */
    public static final int INT_EXPIRES_MINUTE_30 = 30;

    /**
     * 到期10分钟
     */
    public static final int INT_EXPIRES_MINUTE_10 = 10;

    /**
     * sm2的JWT登录验证类
     */
    private static final Sm2JwtSigner SM2_JWT_SIGNER = new Sm2JwtSigner();

    /**
     * 获取当前项目环境
     */
    private final ProfileConfig profileConfig;

    /**
     * 实例化
     *
     * @param profileConfig 配置文件配置
     */
    public JwtTokenUtils(ProfileConfig profileConfig) {
        this.profileConfig = profileConfig;
    }

    /**
     * 创建令牌
     *
     * @param strUserName      用户名
     * @param strUserAuthority 用户权限
     * @param mapProperties    用户扩展属性
     * @param dateExpires      到期时间
     * @return {@link String}
     */
    public String getToken(String strUserName, String strUserAuthority, Map<String, Object> mapProperties, Date dateExpires) {
        Assert.notBlank(strUserName);
        Assert.notBlank(strUserAuthority);
        Assert.notNull(dateExpires);
        return JWT.create().setSigner(SM2_JWT_SIGNER)
                .setAudience(strUserName)
                .setIssuedAt(new Date())
                .setExpiresAt(dateExpires)
                .setPayload(STR_USER_AUTHORITY, strUserAuthority)
                .setPayload(STR_MAP_USER_PROPERTIES, mapProperties)
                .sign();
    }

    /**
     * 创建令牌 - 偏移秒
     *
     * @param strUserName      用户名
     * @param strUserAuthority 用户权限
     * @param mapProperties    用户扩展属性
     * @param intExpiresSecond 到期时间比当前时间偏移秒
     * @return {@link String}
     */
    public String getTokenOffsetSecond(String strUserName, String strUserAuthority, Map<String, Object> mapProperties, Integer intExpiresSecond) {
        // 当前时间+偏移时间为到期时间
        Date dateExpires = DateUtil.offsetSecond(DateUtil.date(), intExpiresSecond);
        return getToken(strUserName, strUserAuthority, mapProperties, dateExpires);
    }

    /**
     * 创建令牌 - 有效期三十分钟
     *
     * @param strUserName      用户名
     * @param strUserAuthority 用户权限
     * @param mapProperties    用户扩展属性
     * @return {@link String}
     */
    public String getToken30Minute(String strUserName, String strUserAuthority, Map<String, Object> mapProperties) {
        return getTokenOffsetSecond(strUserName, strUserAuthority, mapProperties, INT_EXPIRES_MINUTE_30 * 60);
    }

    /**
     * 得到token10分钟
     * 创建令牌 - 有效期三十分钟
     *
     * @param strUserName      用户名
     * @param strUserAuthority 用户权限
     * @param mapProperties    用户扩展属性
     * @return {@link String}
     */
    public String getToken10Minute(String strUserName, String strUserAuthority, Map<String, Object> mapProperties) {
        return getTokenOffsetSecond(strUserName, strUserAuthority, mapProperties, INT_EXPIRES_MINUTE_10 * 60);
    }

    /**
     * 验证令牌
     *
     * @param strToken 标记
     */
    public void verifyToken(String strToken) {
        // 判断token是否存在
        if (StrUtil.isBlank(strToken)) {
            throw new BadCredentialsException(MSG_LOGIN_TOKEN_NOT_EXIST);
        }

        // 校验签名
        try {
            if (!JWTUtil.verify(strToken, SM2_JWT_SIGNER)) {
                throw new BadCredentialsException(MSG_SIGNATURE_FAILED);
            }
        } catch (Exception e) {
            throw new BadCredentialsException(MSG_SIGNATURE_FAILED);
        }

        // 如果当前生产环境，则从缓存中对比token
        // 【备注】这样在测试环境下，重启服务无需重新登录，之前的JWT Token依然有效
        if (StrUtil.equals(profileConfig.getActiveProfile(), ProfileConfig.PROD_PROFILE)) {
            String strToken_ = MAP_SYSTEM_USER_TOKEN.get(getAudience(strToken));
            if (StrUtil.isBlank(strToken_) || !StrUtil.equals(strToken_, strToken)) {
                throw new BadCredentialsException(MSG_LOGIN_TOKEN_NOT_EXIST);
            }
        }

        // 校验时间字段
        try {
            JWTValidator.of(strToken).validateDate(DateUtil.date());
        } catch (ValidateException e) {
            throw new BadCredentialsException(MSG_USER_SIGNATURE_EXPIRED);
        }
    }

    /**
     * 获取签发对象（用户登录名）
     *
     * @param strToken 令牌字符串
     * @return {@link String}
     */
    public String getAudience(String strToken) {
        JSONArray claimByName = getClaimByName(strToken, JWTPayload.AUDIENCE, JSONArray.class);
        return claimByName.getStr(0);
    }

    /**
     * 获取权限字符串
     *
     * @param strToken 令牌字符串
     * @return {@link String}
     */
    public String getAuthorities(String strToken) {
        return getClaimByName(strToken, STR_USER_AUTHORITY, String.class);
    }

    /**
     * 获取用户扩展属性Map
     *
     * @param strToken 令牌字符串
     * @return {@link String}
     */
    public Map<String, Object> getUserPropertiesMap(String strToken) {
        return getClaimByName(strToken, STR_MAP_USER_PROPERTIES, Map.class);
    }

    /**
     * 得到要求名字
     * 通过载荷名字获取载荷的值 为Integer
     * `
     *
     * @param strToken 令牌字符串
     * @param strName  载荷名字
     * @param tClass   返回的类型
     * @return {@link T}
     */
    @SuppressWarnings("unchecked")
    public <T> T getClaimByName(String strToken, String strName, Class<T> tClass) {
        JWTPayload payload = JWTUtil.parseToken(strToken).getPayload();
        if (tClass.equals(Date.class)) {
            return (T) payload.getClaimsJson().getDate(strName);
        } else if (tClass.equals(Integer.class)) {
            return (T) payload.getClaimsJson().getInt(strName);
        } else if (tClass.equals(String.class)) {
            return (T) payload.getClaimsJson().getStr(strName);
        } else {
            return (T) payload.getClaim(strName);
        }
    }

    /**
     * 判断令牌是否待刷新，发行时间-有效时间的2/3后待刷新
     *
     * @param strToken 令牌字符串
     * @return {@link Boolean}
     */
    public Boolean isToRefresh(String strToken) {
        // 发行时间
        long longIssuedAt = getClaimByName(strToken, ISSUED_AT, Date.class).getTime();
        // 有效时间
        long longExpiresAt = getClaimByName(strToken, EXPIRES_AT, Date.class).getTime();
        // 获取待刷新时间
        long longToRefresh = longIssuedAt + (longExpiresAt - longIssuedAt) * 2 / 3;
        return DateUtil.date().getTime() > longToRefresh;
    }
}